package org.atlantis.ymer;

import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.atlantis.ymer.EventHelper.EventDispatcher;
import org.atlantis.ymer.EventHelper.EventDispatcherHolder;
import org.atlantis.ymer.command.PubCommand;
import org.atlantis.ymer.event.ClientEventEmitter;
import org.atlantis.ymer.event.EventCallback;
import org.atlantis.ymer.event.ServerEventEmitter;

@SuppressWarnings({ "unchecked", "rawtypes" })
class ServerEventEmitterImpl implements ServerEventEmitter, EventDispatcher {

	private EventCallback<ClientEventEmitter> connectCallback;
	private EventCallback shutdownCallback;

	private ConcurrentMap<String, ClientEventEmitter> clients = new ConcurrentHashMap<String, ClientEventEmitter>();

	@Override
	public void on(String topic, EventCallback callback) {
		if (AppTopic.CONNECT.equals(topic)) connectCallback = (EventCallback<ClientEventEmitter>) callback;
		else if (AppTopic.SHUTDOWN.equals(topic)) shutdownCallback = callback;
	}

	@Override
	public void un(String topic) {
		if (AppTopic.SHUTDOWN.equals(topic)) shutdownCallback = null;
		else if (AppTopic.CONNECT.equals(topic)) throw new YmerException(
				"Cannot unbind ServerEventEmitter's 'TOPIC_CONNECT' topic.");
	}

	@Override
	public void emit(String topic, Object message) {
		for (ClientEventEmitter client : clients.values())
			client.emit(topic, message);
	}

	@Override
	public void dispatch(String topic, Object message) {
		ClientEventEmitter client = new ClientEventEmitterImpl((WebSession) message);
		clients.put(client.getId(), client);
		if (connectCallback == null) return;
		connectCallback.process(client);
	}

	@Override
	public void dispose() {
		Collection<ClientEventEmitter> clientEventEmitters = clients.values();
		clients.clear();
		if (shutdownCallback != null) shutdownCallback.process(clientEventEmitters);
	}

	private class ClientEventEmitterImpl implements ClientEventEmitter, EventDispatcher {

		private WebSession webSession;
		private ConcurrentMap<String, CallbackType> callbacks = new ConcurrentHashMap<String, CallbackType>();

		ClientEventEmitterImpl(WebSession webSession) {
			this.webSession = webSession;
			((EventDispatcherHolder) webSession).setDispatcher(this);
		}

		@Override
		public void on(String topic, EventCallback callback) {
			callbacks.put(topic, new CallbackType(callback));
		}

		@Override
		public void un(String topic) {
			callbacks.remove(topic);
		}

		@Override
		public void emit(String topic, Object message) {
			PubCommand command = new PubCommand(topic, message);
			webSession.getBindedForwarder().sendMessage(command);
		}

		@Override
		public void dispose() {
			if (!webSession.isConnected()) return;
			webSession.close(DisconnectReason.NORMAL_DISCONNECT);
		}

		@Override
		public String getId() {
			return webSession.getId();
		}

		@Override
		public void dispatch(String topic, Object message) {
			CallbackType callbackType = callbacks.get(topic);
			if (AppTopic.DISCONNECT.equals(topic)) {
				onDisconnect();
				if (callbackType != null) callbackType.callback.process(message);
			} else {
				if (callbackType != null) callbackType.callback.process(((PubCommand) message).getMessage(callbackType.type));
			}

		}

		private void onDisconnect() {
			clients.remove(getId(), this);
			callbacks.clear();
		}
	}

	private static class CallbackType {
		EventCallback callback;
		Class type;

		CallbackType(EventCallback callback) {
			this.callback = callback;
			try {
				this.type = (Class) ((ParameterizedType) callback.getClass().getGenericInterfaces()[0])
						.getActualTypeArguments()[0];
			} catch (Throwable cause) {
				throw new YmerException("Cannot get " + callback.getClass() + "'s parameterized type.");
			}
		}
	}
}
